// PetriDish.m
// By Charles G. Fleming, Educational Computing Services, Allegheny College.
// You may freely copy, distribute and reuse this code. 
// Allegheny College and the author disclaim any warranty of any kind, 
// expressed or implied, as to its fitness for any particular use.
// This work was partially supported by a grant from the Vira Heinz Endowment.

#import "PetriDish.h"
#import "DrawPetriDish.h"

#import <appkit/color.h>
#import <appkit/NXImage.h>
#import <appkit/Window.h>
#import <appkit/TextField.h>
#import <appkit/NXImage.h>
#import <appkit/NXCursor.h>
#import <appkit/View.h>

#import <dpsclient/wraps.h>

#import <stdlib.h>
#import <math.h>
#import <libc.h>

#define    ONECENTIMETER  28.3465                              // Assuming 72 dpi.
#define    ONEMILLIMETER   2.83465;

@implementation PetriDish

- (const char *)inspectorName
{
    return "PetriDishInspector";
}
    
// This is called when the view is resized in Interface Builder and when the OK button is pushed. 
// We will force the shape to be square.  Redraw the petri dish in a new NXImage, then display it.
- setFrame:(const NXRect *)frameRect
{
    NXRect rect;
    float side;

    // Force the view to be a square.
    if(frameRect->size.width < frameRect->size.height)
        side = frameRect->size.width;
    else
        side = frameRect->size.height;
	
    NXSetRect(&rect, frameRect->origin.x, frameRect->origin.y, side, side);    
     self = [super setFrame:&rect];
  
    // Free the old NXImage and set up a new one.
    if(!okButtonPushed)
    {	
        agarRadius = 0.90 * (bounds.size.width/2.0);   // in pixels
        petriDishRadius = 0.0352778 * agarRadius;    // in centimeters  (using 72 dpi)
        [petriDishNXImage free];    
        petriDishNXImage = [[NXImage alloc] initSize:&bounds.size];
        [petriDishNXImage lockFocus];
        drawPetriDish(bounds.size.width, bounds.size.width/2.0, agarRadius, 
                           petriRed, petriGreen, petriBlue);
        [petriDishNXImage unlockFocus];
	
        [gridNXImage free];    
        gridNXImage = [[NXImage alloc] initSize:&bounds.size];
        [gridNXImage lockFocus];
        drawGrid(bounds.size.width, bounds.size.width/2.0, agarRadius, gridDelta);
        [gridNXImage unlockFocus];

        [spotsNXImage free];    
        spotsNXImage = [[NXImage alloc] initSize:&bounds.size];
	if(numSpots)
	    [self drawSpotsInNXImage:numSpots];
        [self display];		
    }		

    if(inspector != nil)
        [inspector perform:@selector(revert:) with:self];   
    okButtonPushed = NO;
    return self;
}   

- initFrame:(NXRect *)frameRect
{
	NXSize cursorSize = {16.0, 16.0};
	
    self = [super initFrame:frameRect];

    // Initialize the instance variables.    
    gridState = NO;
    gridSpacing = 1.0;                                             // centimeters
    agarRadius = 0.90 * (bounds.size.width/2.0);   // in pixels
    petriDishRadius = 0.0352778 * agarRadius;    // in centimeters  (using 72 dpi)
    petriRed = 1.0;
    petriGreen = 1.0;
    petriBlue = 0.4902; 
    agarColor = NXConvertRGBAToColor(petriRed, petriGreen, petriBlue, 1.0);
    spotRed = 0.0;
    spotGreen = 0.0;
    spotBlue = 0.0;
    spotColor = NXConvertRGBAToColor(spotRed, spotGreen, spotBlue, 1.0);
    numSpots = 0;
     
    petriDishNXImage = [[NXImage alloc] initSize:&bounds.size];
    [petriDishNXImage lockFocus];
    drawPetriDish(bounds.size.width, bounds.size.width/2.0, agarRadius, 
                           petriRed, petriGreen, petriBlue);
    [petriDishNXImage unlockFocus];

    spotRadius = 5;                                                      // In millimeters.
    dotRadius = spotRadius * ONEMILLIMETER;       // in pixels
    gridDelta = ONECENTIMETER;                             // in pixels
    
    gridNXImage = [[NXImage alloc] initSize:&bounds.size];
    [gridNXImage lockFocus];
    drawGrid(bounds.size.width, bounds.size.width/2.0, agarRadius, gridDelta);
    [gridNXImage unlockFocus];

    spotsNXImage = [[NXImage alloc] initSize:&bounds.size];

    spotNXImageSize.width = 2 * dotRadius + 2.0;
    spotNXImageSize.height = 2 * dotRadius + 2.0;
    spotNXImage = [[NXImage alloc] initSize:&spotNXImageSize];
    [spotNXImage lockFocus];	
    drawSpot(spotNXImageSize.width, spotNXImageSize.width/2.0, dotRadius, 
	             spotRed, spotGreen, spotBlue);
    [spotNXImage unlockFocus];

    okButtonPushed = NO;
	
    // Set up the cursor.
    clickCursorNXImage = [[NXImage alloc] initSize:&cursorSize];
    [clickCursorNXImage lockFocus];
    drawCursor();
    [clickCursorNXImage unlockFocus];
	
    clickCursor = [[NXCursor allocFromZone:[self zone]] 	
			initFromImage:clickCursorNXImage];
    return self;
}    

// Turn grid on or off.  If it is on, draw it with the specified spacing.  Composites the grid
// onto the petri dish.
- turnGridOn:(BOOL)flag withSpacing:(float)spacing
{
    gridState = flag;
    if(gridState)
    {
        gridSpacing = spacing;                                 // in centimeters
        gridDelta = spacing * ONECENTIMETER;           // in pixels
        [gridNXImage lockFocus];
        drawGrid(bounds.size.width, bounds.size.width/2.0, agarRadius, gridDelta);
        [gridNXImage unlockFocus];
    }
    [self display];
    return self;
}

// Set the radius of the agar.  Draws the petriDishNXImage.  Does not redraw the petri dish.    
- setAgarRadius:(float)radius
{
    petriDishRadius = radius;                                  // in centimeters
    agarRadius = radius * ONECENTIMETER;      // in pixels
    if(agarRadius >bounds.size.width / 2.0)
    {
        agarRadius = bounds.size.width / 2.0; 
        petriDishRadius = agarRadius / ONECENTIMETER;
    }	   

    [petriDishNXImage lockFocus];
    drawPetriDish(bounds.size.width, bounds.size.width/2.0, agarRadius, 
                           petriRed, petriGreen, petriBlue);
    [petriDishNXImage unlockFocus];
    
    // Draw the spots NXImage.
    if(numSpots)
        [self drawSpotsInNXImage:numSpots];	

    // Draw the grid NXImage.
    if(gridState)
    {
        [gridNXImage lockFocus];
        drawGrid(bounds.size.width, bounds.size.width/2.0, agarRadius, gridDelta);
        [gridNXImage unlockFocus];
    }    
    return self;
}

// Set the color of the agar.  Draws the petriDishNXImage.  Does not redraw the petri dish.    
- setAgarColor:(NXColor)color
{
    float alpha = 1.0;

    agarColor = color;
    NXConvertColorToRGBA(agarColor, &petriRed, &petriGreen, &petriBlue, &alpha);
    
    [petriDishNXImage lockFocus];
    drawPetriDish(bounds.size.width, bounds.size.width/2.0, agarRadius, 
                           petriRed, petriGreen, petriBlue);
    [petriDishNXImage unlockFocus];
    return self;
}

// Set the color of the spots.  Redraws the spotNXImage and spotsNXImage. 
// Does not redraw the petri dish.  
- setSpotColor:(NXColor)color
{
    float alpha = 1.0;

    spotColor = color;
    NXConvertColorToRGBA(spotColor, &spotRed, &spotGreen, &spotBlue, &alpha);

    // Draw the spot NXImage.
    [spotNXImage lockFocus];	
    drawSpot(spotNXImageSize.width, spotNXImageSize.width/2.0, dotRadius, 
	             spotRed, spotGreen, spotBlue);
    [spotNXImage unlockFocus];	

    // Draw the spots NXImage.
    if(numSpots)
        [self drawSpotsInNXImage:numSpots];	
    return self;
}

// Set the radius of the spots.  Redraws the spotNXImage and spotsNXImage. 
// Does not redraw the petri dish.  
- setSpotRadius:(float)radius
{
    spotRadius = radius;                                               // In millimeters.
    dotRadius = spotRadius * ONEMILLIMETER;        // In pixels.
    
    if(dotRadius > 0.5 * agarRadius)
    {
        dotRadius = 0.5 * agarRadius;            
        spotRadius = dotRadius / ONEMILLIMETER;
    }
    		
    spotNXImageSize.width = 2 * dotRadius + 2.0;
    spotNXImageSize.height = 2 * dotRadius + 2.0;
    [spotNXImage free];	
    spotNXImage = [[NXImage alloc] initSize:&spotNXImageSize];

    // Draw the spot NXImage.
    [spotNXImage lockFocus];	
    drawSpot(spotNXImageSize.width, spotNXImageSize.width/2.0, dotRadius, 
	             spotRed, spotGreen, spotBlue);
    [spotNXImage unlockFocus];
    
    // Draw the spots NXImage.
    if(numSpots)
        [self drawSpotsInNXImage:numSpots];	
    return self;
}

// Draws the spotNXImage and the spots spotsNXImage.  Does not redraw the petri dish.
- setNumSpots:(int)spots
{
    numSpots = spots;
    
    // Draw the spots NXImage.
    [self drawSpotsInNXImage:numSpots];
    return self;
}		

// Draws the petri dish and the spots and grid if necessary.    
- drawSelf:(NXRect *)rects :(int)count
{
    NXPoint origin = {0.0, 0.0};
 
    [petriDishNXImage composite:NX_COPY toPoint:&origin];
     
    if(numSpots)
          [spotsNXImage composite:NX_SOVER toPoint:&origin];

    if(gridState)
        [gridNXImage composite:NX_SOVER toPoint:&origin];   
    return self;
}

- drawPetriDishWithSpots:(int)spots
{
    [self setNumSpots:spots];
    [self display];
    return self;
}

// Draws the spots in an NXImage for the petri dish.  Does not redraw the petri dish.    
- drawSpotsInNXImage:(int)spots;
{
    float maxRadius, halfWidth, randomRadius, randomAngle, spotHalfWidth ;
    NXPoint origin;
    int spot; 

    // Composite spots into spotsNXImage.		
    [spotsNXImage lockFocus];
    PSsetalpha(0.0);
    NXRectFill(&bounds);
    PSsetalpha(1.0);

    maxRadius = agarRadius - dotRadius;
    halfWidth = bounds.size.width / 2.0;
    spotHalfWidth = spotNXImageSize.width/2.0;
	
    numSpots = spots;
    for (spot = 0; spot < numSpots; spot++)
    {	
        randomRadius =  maxRadius * random() / 2147483647.0;
        randomAngle =  6.28319 * random() / 2147483647.0;
        origin.x = randomRadius * cos(randomAngle) + halfWidth - spotHalfWidth;
        origin.y = randomRadius * sin(randomAngle) + halfWidth - spotHalfWidth;
        [spotNXImage composite:NX_SOVER toPoint:&origin];
    } 
    [spotsNXImage unlockFocus];   
     return self;
}    

- (float)gridSpacing
{
    return gridSpacing;        // In centimeters
}   

- (BOOL)gridState
{
    return gridState;
}

- (float)petriDishRadius
{
    return petriDishRadius;   // In centimeters
}  

- (int)numSpots
{
    return numSpots;
}  

- (float)spotRadius
{
    return spotRadius;         // In millimeters.
}  

- okPushed:(BOOL)flag
{
    okButtonPushed = flag;
    return self;
} 

- setInspector:anObject
{
    inspector = anObject;
    return self;
}    

- (NXColor) agarColor
{
    return agarColor;
}       

- (NXColor) spotColor
{
    return spotColor;
}       

// Increment the count each time we click in the petri dish. 	
- mouseDown:(NXEvent *)theEvent
{
	clicks++;
	if(clickCountTextField)
		[clickCountTextField setIntValue:clicks];
	return self;
}

// Set the count back to zero.
- resetClickCount:sender
{
	clicks = 0;
	if(clickCountTextField)
		[clickCountTextField setIntValue:clicks];
	return self;
}	

- (int)clickCount
{
	return clicks;
}	

- resetCursorRects
{
	[self addCursorRect:&bounds cursor:clickCursor];
    return self;
}

- write:(NXTypedStream *)typedStream
{
    [super write:typedStream];
    NXWriteObject(typedStream, petriDishNXImage);
    NXWriteObject(typedStream, gridNXImage);
    NXWriteObject(typedStream, spotsNXImage);
    NXWriteObject(typedStream, spotNXImage);
	NXWriteObject(typedStream, clickCountTextField);
	NXWriteObject(typedStream, clickCursor);
    NXWriteSize(typedStream, &spotNXImageSize);
    NXWriteTypes(typedStream, "{if}", &gridState, &gridSpacing);
    NXWriteTypes(typedStream, "{ff}", &agarRadius, &petriDishRadius);
    NXWriteTypes(typedStream, "{fff}", &petriRed, &petriGreen, &petriBlue);
    NXWriteTypes(typedStream, "{fff}", &spotRed, &spotGreen, &spotBlue);
    NXWriteTypes(typedStream, "{ii}", &numSpots, &okButtonPushed);
    NXWriteTypes(typedStream, "{ff}", &spotRadius, &dotRadius);
    return self;
}
             
- read:(NXTypedStream *)typedStream
{
    [super read:typedStream];
    petriDishNXImage = NXReadObject(typedStream);  
    gridNXImage = NXReadObject(typedStream);
    spotsNXImage = NXReadObject(typedStream);  
    spotNXImage = NXReadObject(typedStream);
	clickCountTextField = NXReadObject(typedStream);
	clickCursor = NXReadObject(typedStream);
    NXReadSize(typedStream, &spotNXImageSize);
    NXReadTypes(typedStream, "{if}", &gridState, &gridSpacing);
    NXReadTypes(typedStream, "{ff}", &agarRadius, &petriDishRadius);
    NXReadTypes(typedStream, "{fff}", &petriRed, &petriGreen, &petriBlue);
    NXReadTypes(typedStream, "{fff}", &spotRed, &spotGreen, &spotBlue);
    NXReadTypes(typedStream, "{ii}", &numSpots, &okButtonPushed);
    NXReadTypes(typedStream, "{ff}", &spotRadius, &dotRadius);
    return self;
}

- awake
{
    agarColor = NXConvertRGBAToColor(petriRed, petriGreen, petriBlue, 1.0);
    spotColor =	NXConvertRGBAToColor(spotRed, spotGreen, spotBlue, 1.0);
	clicks = 0;
    return self;
}
    
@end